#include "stdafx.h"
#include "math/convert_unit.h"
#include "scene/frustum.h"
#include "core/logger.h"

namespace natrium {
	namespace scene {
			Frustum::Frustum(eProjection type):
				mProjectionType(type)
			{
				//generate vertices for the drawable part

				GLuint indices[24] = {
					0, 5,
					0, 6,
					0, 7,
					0, 8,

					1, 2,
					2, 3,
					3, 4,
					4, 5,

					5, 6,
					6, 7,
					7, 8,
					8, 5
				}; //we're drawing lines (knotted pyramid)

				//default to starting at (0, 0, 1) and looking at (0, 0, 0)
				lookAt(
					glm::vec3(0.0f, 0.0f, 1.0f), 
					glm::vec3(0.0f, 0.0f, 0.0f),
					glm::vec3(0.0f, 1.0f, 0.0f)
				);

				if (type == ORTHOGRAPHIC)
					setOrthoBounds(-1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f); //default to [-1..1] for all axes in orthographic mode
				else
					setPerspective(45.0f, 1.0f, 0.1f, 100.0f); //default to 45 degree fov, square aspect with near 0.1 and far 100.0
			
				glBindVertexArray(0); //use the default vertex array for this [?]
				glGenBuffers(2, mHandle); //allocate 2 buffers (at this point we only add the vertices)
				glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mHandle[1]);
				glBufferData(GL_ELEMENT_ARRAY_BUFFER, 24 * sizeof(GLuint), indices, GL_STATIC_DRAW);
			}

			void Frustum::lookAt(const glm::vec3& origin, const glm::vec3& at, const glm::vec3& up){
				mPosition = origin;
				mCenter = at;
				mUp = up;
			}

			void Frustum::setOrthoBounds(float xMin, float xMax, float yMin, float yMax, float zMin, float zMax) {
				mXMin = xMin;
				mXMax = xMax;

				mYMin = yMin;
				mYMax = yMax;

				mZNear = zMin;
				mZFar = zMax;
			}

			void Frustum::setPerspective(float fovy, float aspect, float zNear, float zFar) {
				mFOVy = fovy;
				mAspect = aspect;
				mZNear = zNear;
				mZFar = zFar;
			}

			void Frustum::enclose(const Frustum& f) {
				glm::vec3 n = glm::normalize(f.mPosition - f.mCenter);
				glm::vec3 u = glm::normalize(glm::cross(f.mUp, n));
				glm::vec3 v = glm::normalize(glm::cross(n, u));

				if (mProjectionType == PERSPECTIVE)
					lookAt(mPosition, f.getCenter(), mUp);

				glm::mat4 view = getViewMatrix();
			
				glm::vec3 corner[8];

				if (f.mProjectionType == PERSPECTIVE) {
					float dy = f.mZNear * tanf(math::toRadians(f.mFOVy) * 0.5f);
					float dx = f.mAspect * dy;

					glm::vec3 center = f.mPosition - n * f.mZNear;
				
					corner[0] = center + u * dx + v * dy;
					corner[1] = center - u * dx + v * dy;
					corner[2] = center - u * dx - v * dy;
					corner[3] = center + u * dx - v * dy;

					dy = f.mZFar * tanf(math::toRadians(f.mFOVy) * 0.5f);
					dx = f.mAspect * dy;
					center = f.mPosition - n * f.mZFar;

					corner[4] = center + u * dx + v * dy;
					corner[5] = center - u * dx + v * dy;
					corner[6] = center - u * dx - v * dy;
					corner[7] = center + u * dx - v * dy;
				}
				else {
					glm::vec3 center = f.mPosition - n * f.mZNear;

					corner[0] = center + u * f.mXMax + v * f.mYMax;
					corner[1] = center + u * f.mXMax + v * f.mYMin;
					corner[2] = center + u * f.mXMin - v * f.mYMin;
					corner[3] = center + u * f.mXMin - v * f.mYMax;

					center = f.mPosition - n * f.mZFar;

					corner[0] = center + u * f.mXMax + v * f.mYMax;
					corner[1] = center + u * f.mXMax + v * f.mYMin;
					corner[2] = center + u * f.mXMin + v * f.mYMin;
					corner[3] = center + u * f.mXMin + v * f.mYMax;
				}

				//now we have enough information to ajdust the frustum
				if (mProjectionType == PERSPECTIVE) {
					mFOVy = 0.0f;
					mZNear = std::numeric_limits<float>::max();
					mZFar = 0.0f;

					float maxAngle = 0.0f;

					for (int i = 0; i < 8; ++i) {
						//convert to local space
						glm::vec4 local = view * glm::vec4(corner[i], 1.0f);

						//see if the projection yielded something in front of the view (= negative Z value)
						if (local.z < 0.0f) {
							float d = -local.z;
							float angle = atanf(fabsf(local.x) / d);

							if (angle > maxAngle)
								maxAngle = angle;

							angle = math::toDegrees(atanf(fabsf(local.y) / d));
							if (angle * 2.0f > mFOVy)
								mFOVy = angle * 2.0f;

							if (mZNear > d)
								mZNear = d;
							if (mZFar < d)
								mZFar = d;
						}
					}

					float height = (mZNear * tanf(math::toRadians(mFOVy) * 0.5f)) * 2.0f;
					float width = (mZNear * tanf(maxAngle)) * 2.0f;

					mAspect = width / height;
				}
				else {
					mXMin = std::numeric_limits<float>::max();
					mYMin = std::numeric_limits<float>::max();
					mZNear = std::numeric_limits<float>::max();
					mXMax = -std::numeric_limits<float>::max();
					mYMax = -std::numeric_limits<float>::max();
					mZFar = -std::numeric_limits<float>::max();

					for (int i = 0; i < 8; ++i) {
						glm::vec4 local = view * glm::vec4(corner[i], 1.0f);

						if (mXMin > local.x)
							mXMin = local.x;
						if (mXMax < local.x)
							mXMax = local.x;
						if (mYMin > local.y)
							mYMin = local.y;
						if (mYMax < local.y)
							mYMax = local.y;
						if (mZNear > local.z)
							mZNear = local.z;
						if (mZFar < local.z)
							mZFar = local.z;
					}
				}

			}

			glm::mat4 Frustum::getViewMatrix() const {
				return glm::lookAt(mPosition, mCenter, mUp);
			}

			glm::mat4 Frustum::getProjectionMatrix() const {
				if (mProjectionType == PERSPECTIVE)
					return glm::perspective(mFOVy, mAspect, mZNear, mZFar);
				else
					return glm::ortho(mXMin, mXMax, mYMin, mYMax, mZNear, mZFar);
			}

			glm::vec3 Frustum::getOrigin() const {
				return mPosition;
			}

			glm::vec3 Frustum::getCenter() const {
				float distanceToCenter = 0.5f * (mZNear + mZFar);
				glm::vec3 relative = glm::normalize(mCenter - mPosition);
				return mPosition + (distanceToCenter * relative);
			}

			void Frustum::listInfo() const {
				gLog.info() << "== Frustum info ==";

				if (mProjectionType == PERSPECTIVE)
					gLog.info() << "Perspective: FOV = " <<  mFOVy << ", Aspect = " << mAspect << ", Near = " << mZNear << ", Far = " << mZFar;
				else
					gLog.info() << "Orthographic: X(" << mXMin << ", " << mXMax << ") Y(" << mYMin << ", " << mYMax << ") Z(" << mZNear << ", " << mZFar << ")";

				gLog.info() << 
					"Position: (" << mPosition.x << ", " << mPosition.y << ", " << mPosition.z << 
					") Target(" << mCenter.x << ", " << mCenter.y << ", " << mCenter.z <<
					") Up(" << mUp.x << ", " << mUp.y << ", " << mUp.z << ")";
			}

			void Frustum::render() const {
				if (mProjectionType == PERSPECTIVE) {
					static float vertices[9 * 3];
					static glm::vec3 corner[8];

					vertices[0] = mPosition.x;
					vertices[1] = mPosition.y;
					vertices[2] = mPosition.z;

					glm::vec3 n = glm::normalize(mPosition - mCenter);
					glm::vec3 u = glm::normalize(glm::cross(mUp, n));
					glm::vec3 v = glm::normalize(glm::cross(n, u));

					float dy = mZNear * tanf(math::toRadians(mFOVy) * 0.5f);
					float dx = dy * mAspect;

					glm::vec3 c = mPosition - n * mZNear; //center of near plane

					corner[0] = c + u * dx + v * dy;
					corner[1] = c - u * dx + v * dy;
					corner[2] = c - u * dx - v * dy;
					corner[3] = c + u * dx - v * dy;

					dy = mZFar * tanf(math::toRadians(mFOVy) * 0.5f);
					dx = dy * mAspect;

					c = mPosition - n * mZFar; //center of far plane

					corner[4] = c + u * dx + v * dy;
					corner[5] = c - u * dx + v * dy;
					corner[6] = c - u * dx - v * dy;
					corner[7] = c + u * dx - v * dy;

					size_t idx = 3; //skip the first vertex
					for (int i = 0; i < 8; ++i) {
						vertices[idx++] = corner[i].x;
						vertices[idx++] = corner[i].y;
						vertices[idx++] = corner[i].z;
					}

					glBindVertexArray(0);

					glBindBuffer(GL_ARRAY_BUFFER, mHandle[0]);
					glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_DYNAMIC_DRAW); //use DYNAMIC_DRAW as this buffer will change often
					glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
					glEnableVertexAttribArray(0);

					glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mHandle[1]);
					glDrawElements(GL_LINES, 24, GL_UNSIGNED_INT, 0);
				}
			
				//[TODO] -- orthographic
			}
	}
}